﻿//-------------------------------------
// WBFSSync - WBFSSync.exe
//
// Copyright 2009 Caian (ÔmΣga Frøst) <frost.omega@hotmail.com>
//
// WBFSSync is Licensed under the terms of the Microsoft Reciprocal License (Ms-RL)
//
// OperationManager.cs:
//
// Classe que cria e gerencia todas as operações que são feitas pelo WBFSSync
//
// Nota: a classe IOManager controla operações de leitura e escrita, a OperationManager controla tipos específicos
// de operação mas ainda está associada ao IOManager para verificação de uso de arquivos
//
// A implementação do UID serve para acelerar a adição / remoção utilizando uma tabela de hash
// e como nome de ListViewItems em OperationListView
//
//-------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Threading;
using System.IO;
using System.Diagnostics;
using System.Security.Cryptography;

namespace WBFSSync
{
    public enum OperationType : byte
    {
        Unknown,

        Copy,
        Move,
        Delete,
        DiscDataChange,

        RarDecompress,
        ScrubDecompress,
        ScrubCompress,

        Format,
        DeviceCloning,

        CoverDownload,
        ProgramUpdate,
    }

    delegate void OperationEventDelegate(Operation operation);
    delegate void ClipboardChangedDelegate(String[] clipboard);

    delegate CopyProgressResult CopyProgressRoutine( long TotalFileSize, long TotalBytesTransferred, 
        long StreamSize, long StreamBytesTransferred, uint dwStreamNumber, CopyProgressCallbackReason dwCallbackReason,
        int hSourceFile, int hDestinationFile, Operation lpData);

    enum CopyProgressResult : uint
    {
        PROGRESS_CONTINUE = 0,
        PROGRESS_CANCEL = 1,
        PROGRESS_STOP = 2,
        PROGRESS_QUIET = 3
    }

    enum CopyProgressCallbackReason : uint
    {
        CALLBACK_CHUNK_FINISHED = 0x00000000,
        CALLBACK_STREAM_SWITCH = 0x00000001
    }

    [Flags]
    enum CopyFileFlags : uint
    {
        None = 0,
        COPY_FILE_FAIL_IF_EXISTS = 0x00000001,
        COPY_FILE_RESTARTABLE = 0x00000002,
        COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x00000004,
        COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x00000008
    }

    [Flags]
    enum MoveFileFlags : uint
    {
        None = 0,
        MOVE_FILE_REPLACE_EXISTSING = 0x00000001,
        MOVE_FILE_COPY_ALLOWED = 0x00000002,
        MOVE_FILE_DELAY_UNTIL_REBOOT = 0x00000004,
        MOVE_FILE_WRITE_THROUGH = 0x00000008,
        MOVE_FILE_CREATE_HARDLINK = 0x00000010,
        MOVE_FILE_FAIL_IF_NOT_TRACKABLE = 0x00000020
    }

    struct THREADINFO
    {
        public String Name;
        public Boolean Working;

        public THREADINFO(String name, Boolean working)
        {
            Name = name;
            Working = working;
        }
    }

    static class OperationManager
    {
        //----------------- Constantes

        //----------------- Operações

        const ushort UID_COVEREXPORT = 0xFFF0;
        const ushort UID_COVERDOWNLOAD = 0xFFF1;
        const ushort UID_PROGRAMUPDATE = 0xFFF2;

        //----------------- Clipboard

        const int WM_DRAWCLIPBOARD = 776; //Alteração da clipboard
        const int WM_CHANGECBCHAIN = 781;
        
        //----------------- Eventos
        public static event OperationEventDelegate OperationCreated;
        public static event OperationEventDelegate OperationActivated;
        public static event OperationEventDelegate OperationEnd;
        public static event ClipboardChangedDelegate ClipboardChanged;

        //----------------- Variáveis

        //------- Clipboard
        static IntPtr thisMonitor = IntPtr.Zero;
        static IntPtr nextMonitor = IntPtr.Zero; //Proxima janela a monitorar o clipboard

        static String[] DiscClipboard = new String[0];
        static Boolean CutClipboard = false;
        public static Boolean CanPaste { get { return DiscClipboard.Length > 0; } }

        //------- Operações

        static List<Operation> OperationList = new List<Operation>();
        static Boolean OperationListLocked = false;

        //------- Operações exclusivas

        static Operation UpdateOperation = null;
        static Operation CoverDownloadOperation = null;
        static Operation CoverExportOperation = null;

        //

        static Boolean ShutDown = false;

        static int PcOnlyOperations = 0;
        static int WbfsOperations = 0;

        static int activeOperations = 0;
        static int totalOperations = 0;
        public static int ActiveOperations { get { return activeOperations; } }
        public static int TotalOperations { get { return totalOperations; } }

        //------- Configurações

        static Boolean IgnoreWindowsClipboard = false;
        public static Boolean ExtractDiscScrubbed = false; // Ao extrair uma ISO ou Ripar do DVD salva como SyncScrub
        public static PartitionSelection ScrubPartition = PartitionSelection.OnlyGamePartition;

        public static int MaximumIOsPerWinDrive = 6;
        static int MaximumIOsPerWBFSDrive = 1;

        public static int OperationTimeout = 5000;

        //----------------- Threads... eu não conheço uma tradução decente pra esse termo...

        static THREADINFO[] WbfsThreads = null;
        static THREADINFO PcOnlyThread = new THREADINFO("PC", false);

        //------- Threads exclusivas

        static THREADINFO UpdateThread = new THREADINFO("UPDATE", false);
        static THREADINFO CoverDownloadThread = new THREADINFO("COVERDOWNLOAD", false);
        static THREADINFO CoverExportThread = new THREADINFO("COVEREXPORT", false);

        //----------------- Rotinas

        //----------------- Gera um id para a operação
        static ushort GenerateUid()
        {
            return GenerateUid(false);
        }

        static ushort GenerateUid(bool nolock)
        {
            if (!nolock) LockOperationList();

            ushort uid = 1;

        SEARCHUID:

            //UIDs a partir de 0xFFF0 (inclusive ele) são reservados para operações especiais

            for (int i = 0; i < OperationList.Count; i++)
            {
                if (OperationList[i].UniqueID == uid)
                {
                    uid++;
                    goto SEARCHUID;
                }
            }

            if (!nolock) UnlockOperationList();

            return uid;
        }

        //----------------- Mutex da lista de operações

        //----------------- Tranca a lista de operações
        static void LockOperationList()
        {
            while (OperationListLocked) Thread.Sleep(1);
            OperationListLocked = true;
        }

        //----------------- Destranca a lista de operações
        static void UnlockOperationList()
        {
            OperationListLocked = false;
        }

        //----------------- Rotinas de verificação

        //----------------- Verifica se o arquivo existe
        static bool FileExists(String file)
        {
            String drivename = Path.GetPathRoot(file);
            if (drivename == String.Empty) return false;

            IDrive drive = DeviceManager.GetDrive(drivename);
            if (drive == null) File.Exists(file);

            if (drive.Is(DriveFlags.WBFS))
            {
                if (((WBFSDrive)drive)._Device.GetDiscByCode(Path.GetFileName(file)) != null)
                    return true;
                else
                    return false;
            }
            else if (drive is CDDrive)
            {
                CDDrive cddrive = (CDDrive)drive;
                return String.Compare(file, cddrive.Name + cddrive._Code + " - " + cddrive._Name, true) == 0;
            }
            else
            {
                return File.Exists(file);
            }
        }

        //----------------- Verifica se há espaço livre no destino e retorna o espaço a ser utilizado
        static long EnsureFreeSpace(String fileout, long size)
        {
            string sdrive = Path.GetPathRoot(fileout);
            if (sdrive == String.Empty) return -1; //Arquivo inválido

            IDrive drive = DeviceManager.GetDrive(sdrive);
            if (drive == null) return -1; //Drive não existe

            if (drive.Is(DriveFlags.WBFS))
            {
                if (((WBFSDrive)drive)._Device.GetDiscByCode(Path.GetFileName(fileout)) != null)
                {
                    return -1;
                }
                else
                {
                    return size;
                }
            }
            else
            {
                if (File.Exists(fileout))
                {
                    FileInfo fi = new FileInfo(fileout);
                    size -= fi.Length;
                    fi = null;
                }

                return size;
            }
        }

        //----------------- Substitui os atalhos especiais e valida o caminho
        private static string FixPath(String path)
        {
            //Traduz o destinho se for pasta especial, cancela a operação se o destino for 'meu computador' e 'capas'
            //colar na livraria só será valido se ela possuir pelo menos uma pasta
            if (path[0] == '#')
            {
                switch (path)
                {
                    case FormWBFSSync.path_computer:
                    case FormWBFSSync.path_covers:

                        MessageBox.Show(Language.message_invalid_folder, Language.AppName, MessageBoxButtons.OK,
                            MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);

                        return String.Empty;

                    case FormWBFSSync.path_library:

                        if (Library.Folders.Count == 0)
                        {
                            MessageBox.Show(Language.message_invalid_folder, Language.AppName, MessageBoxButtons.OK,
                                MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);

                            return String.Empty;
                        }
                        else
                        {
                            path = Library.Folders[0].Path;
                        }

                        break;
                }
            }

            //
            if (!path.EndsWith("\\")) path += '\\';

            //
            return path;
        }

        //----------------- Converte um nome de jogo para nome válido no sistema operacional
        public static String FixName(String name)
        {
            StringBuilder newname = new StringBuilder(name.Length);
            //Retira qualquer outro caractere
            for (int i = 0; i < name.Length; i++)
            {
                switch (name[i])
                {
                    case ':': newname.Append(" -"); break;
                    case '<': newname.Append('('); break;
                    case '>': newname.Append(')'); break;
                    case '"': newname.Append('\''); break;
                    case '/':
                    case '\\':
                    case '|':
                    case '*':
                    case '?': break;
                    default: newname.Append(name[i]); break;
                }
            }

            return newname.ToString();
        }

        //----------------- Gerenciador de estado das operações

        //----------------- Conta o número de operações WBFS
        static int CountWbfsOperations(string drive)
        {
            LockOperationList();

            int count = 0;
            foreach (Operation o in OperationList)
            {
                //Se a origem for wbfs:
                // WBFS->PC, a origem é o drive wbfs em questão
                // WBFS->WBFS, o destino é o drive wbfs em questão
                //
                //Se a origem não for wbfs:
                // PC->WBFS ou DVD->WBFS, o destino é o drive wbfs em questão

                String odrive = (o.IsSourceWBFS && !o.IsDestinationWBFS) ? o.SourceDrive.Name : o.DestinationDrive.Name;

                if (String.Compare(drive, odrive, true) == 0)
                {
                    count++;
                }
            }

            UnlockOperationList();
            return count;
        }

        //----------------- Retorna a primeira operação wbfs livre na lista
        static Operation GetWbfsOperation(int order, string drive)
        {
            LockOperationList();

            int j = 0;
            foreach (Operation o in OperationList)
            {
                //Se a origem for wbfs:
                // WBFS->PC, a origem é o drive wbfs em questão
                // WBFS->WBFS, o destino é o drive wbfs em questão
                //
                //Se a origem não for wbfs:
                // PC->WBFS ou DVD->WBFS, o destino é o drive wbfs em questão

                String odrive = (o.IsSourceWBFS && !o.IsDestinationWBFS) ? o.SourceDrive.Name : o.DestinationDrive.Name;

                if (!o.InUse && (String.Compare(drive, odrive, true) == 0) &&
                    (o.SourceDrive == null ? true : o.SourceDrive.Is(DriveFlags.WBFS)) ||
                    (o.DestinationDrive == null ? true : o.DestinationDrive.Is(DriveFlags.WBFS)))
                {
                    if (j++ < order) continue;

                    o.InUse = true;
                    UnlockOperationList();
                    return o;
                }
            }

            UnlockOperationList();
            return null;
        }

        //----------------- Retorna a primeira operação não wbfs livre na lista
        static Operation GetPcOnlyOperation(int order)
        {
            LockOperationList();

            int j = 0;
            foreach (Operation o in OperationList)
            {
                if (!o.InUse &&
                    (o.SourceDrive == null ? true : !o.SourceDrive.Is(DriveFlags.WBFS)) &&
                    (o.DestinationDrive == null ? true : !o.DestinationDrive.Is(DriveFlags.WBFS)))
                {
                    if (j++ < order) continue;

                    o.InUse = true;
                    UnlockOperationList();
                    return o;
                }
            }

            UnlockOperationList();
            return null;
        }

        //----------------- Libera o marcador de uso da operação
        static void FreeOperation(Operation o)
        {
            LockOperationList();
            o.InUse = false;
            UnlockOperationList();
        }

        //----------------- Ativa uma operação
        static void ActivateOperation(Operation o)
        {
            LockOperationList();

            o.Waiting = false;
            activeOperations++;

            if (o.SourceDrive != null)
            {
                o.SourceDrive.ActiveOperations++;
            }

            if ((o.DestinationDrive != null) && (o.SourceDrive != o.DestinationDrive))
            {
                o.DestinationDrive.ActiveOperations++;
            }

            UnlockOperationList();

            if (OperationActivated != null) OperationActivated(o);
        }

        //----------------- Remove a operação da lista
        static void FinishOperation(Operation o)
        {
            LockOperationList();

            if (!o.Waiting)
            {
                activeOperations--;

                if (o.SourceDrive != null)
                {
                    o.SourceDrive.ActiveOperations--;
                    o.SourceDrive.Refresh();
                }

                if ((o.DestinationDrive != null) && (o.SourceDrive != o.DestinationDrive))
                {
                    o.DestinationDrive.ActiveOperations--;
                    o.DestinationDrive.Refresh();
                }

                if ((o.SourceDrive == null ? true : o.SourceDrive.Is(DriveFlags.WBFS)) ||
                    (o.DestinationDrive == null ? true : o.DestinationDrive.Is(DriveFlags.WBFS)))
                {
                    WbfsOperations--;
                }
                else
                {
                    PcOnlyOperations--;
                }
            }

            OperationList.Remove(o);
            totalOperations--;

            UnlockOperationList();

            if (OperationEnd != null) OperationEnd(o);
        }

        //----------------- Registra a operação na fila
        static void RegisterOperation(Operation o)
        {
            RegisterOperation(o, false);
        }

        static void RegisterOperation(Operation o, bool nolock)
        {
            if (!nolock) LockOperationList();

            if ((o.SourceDrive == null ? true : o.SourceDrive.Is(DriveFlags.WBFS)) ||
                (o.DestinationDrive == null ? true : o.DestinationDrive.Is(DriveFlags.WBFS)))
            {
                WbfsOperations++;
            }
            else
            {
                PcOnlyOperations++;
            }

            OperationList.Add(o);
            totalOperations++;

            if (!nolock) UnlockOperationList();

            OnCreateOperation(o);
        }

        //----------------- Dispara o evento 
        static void OnCreateOperation(Operation o)
        {
            if (OperationCreated != null) OperationCreated(o);
        }

        //----------------- Verifica se a operação pode ser realizada
        static bool CanDoOperation(Operation o)
        {
            if (o.SourceDrive != null)
            {
                int maxops = o.SourceDrive.Is(DriveFlags.WBFS) ? MaximumIOsPerWBFSDrive : MaximumIOsPerWinDrive;
                if ((!o.SourceDrive.Ready) || (o.SourceDrive.ActiveOperations >= maxops)) return false;
            }

            if (o.DestinationDrive != null)
            {
                int maxops = o.DestinationDrive.Is(DriveFlags.WBFS) ? MaximumIOsPerWBFSDrive : MaximumIOsPerWinDrive;
                if ((!o.DestinationDrive.Ready) || (o.DestinationDrive.ActiveOperations >= maxops)) return false;
            }

            if (o.SourcePath.Length > 0)
            {
                //Origem em uso
                if (IOManager.ContainsHandle(o.SourcePath)) return false;
            }

            if (o.DestinationPath.Length > 0)
            {
                //Destino em uso
                if (IOManager.ContainsHandle(o.DestinationPath)) return false;
            }

            return true;
        }

        //----------------- Cancela todas as operações, ativas ou pendentes
        public static void StopAllOperations()
        {
            //Cancela todas as operações
            LockOperationList();
            for (int i = 0; i < OperationList.Count; i++) OperationList[i].Progress.Cancel = true;
            UnlockOperationList();

            if (UpdateOperation != null) UpdateOperation.Progress.Cancel = true;
            if (CoverDownloadOperation != null) CoverDownloadOperation.Progress.Cancel = true;
            if (CoverExportOperation != null) CoverExportOperation.Progress.Cancel = true;


            //Espera as threads retornarem
            ShutDown = true;

            //Espera até que todas as operações sejam removidas ou que ocorra um timeout
            long before = DateTime.Now.Ticks;
            while ((OperationList.Count > 0) &&
                   (TimeSpan.FromTicks(DateTime.Now.Ticks - before).TotalMilliseconds < OperationTimeout))
            {
                if (ThreadManager.CountBusyThreads() == 0) break;
                Thread.Sleep(1);
            }

            //Verifica se todas as operações terminaram, se não terminaram, limpa a lista
            //e aborta as threads ativas

            if (OperationList.Count > 0)
            {
                LockOperationList();
                OperationList.Clear();
                UnlockOperationList();
            }

            ThreadManager.AbortAllThreads();
        }

        //----------------- Se assegura de que o drive WBFS possui uma thread funcionando
        public static void EnsureWBFSThread(String drive)
        {
            int i;
            //

            if (WbfsThreads == null) //Aloca matriz de threads
            {
                WbfsThreads = new THREADINFO[DeviceManager.WBFSDriveCount];
            }
            else if (WbfsThreads.Length != DeviceManager.WBFSDriveCount) //Redimensiona matriz
            {
                THREADINFO[] temp = WbfsThreads;
                WbfsThreads = new THREADINFO[DeviceManager.WBFSDriveCount];

                int j = 0;
                for (i = 0; i < temp.Length; i++)
                {
                    if (DeviceManager.GetDrive(temp[i].Name) != null)
                    {
                        WbfsThreads[j++] = temp[i];
                    }
                }
            }

            //Verifica se uma thread para o respectivo drive já está em execução
            for (i = 0; i < WbfsThreads.Length; i++)
                if ((String.Compare(WbfsThreads[0].Name, drive, true) == 0) && (WbfsThreads[i].Working)) return;

            //Cria uma thread
            for (i = 0; i < WbfsThreads.Length; i++)
            {
                //Pula todas as threads em funcionamento
                if (WbfsThreads[i].Working) continue;

                //Achou um indice livre, reinicia a thread
                WbfsThreads[i].Name = drive;
                WbfsThreads[i].Working = true;                
                ThreadManager.LaunchThread("Operation." + drive, new ParameterizedThreadStart(WbfsWorker), drive);

                return;

            }

            //O gerenciador não encontrou nenhum indice livre, ou seja, erro...
            if (i == WbfsThreads.Length)
            {
                //Loga erro aqui
            }
        }

        //----------------- Se assegura de que a thread PcOnlyThread está funcionando
        public static void EnsurePcOnlyThread()
        {
            if (!PcOnlyThread.Working)
            {
                PcOnlyThread.Working = true;
                ThreadManager.LaunchThread("Operation.PC", new ThreadStart(PcOnlyWorker));
            }
        }

        //----------------- Operações

        //----------------- Cria uma operação de download de capas
        public static void DownloadCovers()
        {
            if (Covers.CoverDownloadActive || (Covers.CoversToDownload.Count == 0)) return;

            CoverDownloadOperation = new Operation(UID_COVERDOWNLOAD);

            CoverDownloadOperation.Type = OperationType.CoverDownload;

            CoverDownloadOperation.SourceDrive = null;
            CoverDownloadOperation.SourcePath = Covers.DownloadHost;
            CoverDownloadOperation.DestinationDrive = null;
            CoverDownloadOperation.DestinationPath = Covers.CoversDir;

            CoverDownloadThread.Working = true;
            ThreadManager.LaunchThread("Operation.CDwnld", new ParameterizedThreadStart(CoverDownloadRoutine), 
                CoverDownloadOperation);

            totalOperations++;
            OnCreateOperation(CoverDownloadOperation);
        }

        //----------------- Cria uma operação de atualização do programa
        public static void UpdateProgram()
        {
            if (ProgramUpdate.UpdateActive) return;

            UpdateOperation = new Operation(UID_PROGRAMUPDATE);

            UpdateOperation.Type = OperationType.ProgramUpdate;

            UpdateOperation.SourceDrive = null;
            UpdateOperation.SourcePath = ProgramUpdate.UpdateHost;
            UpdateOperation.DestinationDrive = null;
            UpdateOperation.DestinationPath = "";

            UpdateThread.Working = true;
            ThreadManager.LaunchThread("Operation.Updt", new ParameterizedThreadStart(ProgramUpdateRoutine), 
                UpdateOperation);

            totalOperations++;
            OnCreateOperation(UpdateOperation);
        }

        //----------------- Cria uma operação para exportar capas
        public static void ExportCovers(int Loader, String Drive)
        {
            CoverExportOperation = new Operation(UID_COVEREXPORT);

            CoverExportOperation.Type = OperationType.Copy;

            CoverExportOperation.SourceDrive = null;
            CoverExportOperation.SourcePath = Covers.CoversDir;
            CoverExportOperation.DestinationDrive = null;
            CoverExportOperation.DestinationPath = Drive;
            if (Loader >= 0)
            {
                CoverExportOperation.DestinationPath += Covers.Loaders[Loader].Path;
                CoverExportOperation.ExtendedInfo = Covers.Loaders[Loader];
            }

            CoverExportThread.Working = true;
            ThreadManager.LaunchThread("Operation.CExprt", new ParameterizedThreadStart(ExportCoversRoutine), 
                CoverExportOperation);

            totalOperations++;
            OnCreateOperation(CoverExportOperation);
        }

        //----------------- Copia arquivos para a DiscClipboard
        public static void Copy(string[] files)
        {
            DiscClipboard = files;
            CutClipboard = false;
            if (ClipboardChanged != null) ClipboardChanged(DiscClipboard);
        }

        //----------------- Move arquivos para a DiscClipboard
        public static void Cut(string[] files)
        {
            DiscClipboard = files;
            CutClipboard = true;
            if (ClipboardChanged != null) ClipboardChanged(DiscClipboard);
        }

        //----------------- Cola arquivos para da DiscClipboard
        public static void Paste(string destination)
        {
            MoveFiles(DiscClipboard, destination, CutClipboard);

            if (CutClipboard)
            {
                DiscClipboard = new String[0];
                if (ClipboardChanged != null) ClipboardChanged(DiscClipboard);
            }
        }

        //----------------- Move arquivos
        public static void MoveFiles(String[] files, String destination, bool deleteSource)
        {
            //Caminho vazio não...
            if (destination.Length == 0) return;

            //Valida o caminho de destino
            if ((destination = FixPath(destination)) == String.Empty) return;

            IDrive source = null;
            IDrive dest = DeviceManager.GetDrive(Path.GetPathRoot(destination));

            LockOperationList();
            
            for (int i = 0; i < files.Length; i++)
            {
                source = DeviceManager.GetDrive(Path.GetPathRoot(files[i]));

                Operation operation = new Operation(GenerateUid(true));
                operation.SourcePath = files[i];
                operation.SourceDrive = source;
                operation.DestinationPath = destination;
                operation.DestinationDrive = dest;
                operation.Type = deleteSource ? OperationType.Move : OperationType.Copy;

                RegisterOperation(operation, true);

                if (source.Is(DriveFlags.WBFS) || dest.Is(DriveFlags.WBFS))
                {
                    String odrive = (source.Is(DriveFlags.WBFS) && !dest.Is(DriveFlags.WBFS)) ? source.Name : dest.Name;
                    EnsureWBFSThread(odrive);
                }
                else
                {
                    EnsurePcOnlyThread();
                }
            }
            
            UnlockOperationList();

        }

        //----------------- Comprime uma iso para scrub
        public static void Compress(String[] files, String destination)
        {
            //Caminho vazio não...
            if (destination.Length == 0) return;

            //Valida o caminho de destino
            if ((destination = FixPath(destination)) == String.Empty) return;

            IDrive source = null;
            IDrive dest = DeviceManager.GetDrive(Path.GetPathRoot(destination));

            LockOperationList();

            for (int i = 0; i < files.Length; i++)
            {
                source = DeviceManager.GetDrive(Path.GetPathRoot(files[i]));

                Operation operation = new Operation(GenerateUid(true));
                operation.SourcePath = files[i];
                operation.SourceDrive = source;
                operation.DestinationPath = destination;
                operation.DestinationDrive = dest;
                operation.Type = OperationType.ScrubCompress;

                RegisterOperation(operation, true);

                if (source.Is(DriveFlags.WBFS))
                {
                    EnsureWBFSThread(source.Name);
                }
                else
                {
                    EnsurePcOnlyThread();
                }
            }

            UnlockOperationList();
        }

        //----------------- Comprime um scrub para iso
        public static void Decompress(String[] files, String destination)
        {
            //Caminho vazio não...
            if (destination.Length == 0) return;

            //Valida o caminho de destino
            if ((destination = FixPath(destination)) == String.Empty) return;

            IDrive source = null;
            IDrive dest = DeviceManager.GetDrive(Path.GetPathRoot(destination));

            LockOperationList();

            for (int i = 0; i < files.Length; i++)
            {
                source = DeviceManager.GetDrive(Path.GetPathRoot(files[i]));

                Operation operation = new Operation(GenerateUid(true));
                operation.SourcePath = files[i];
                operation.SourceDrive = source;
                operation.DestinationPath = destination;
                operation.DestinationDrive = dest;
                operation.Type = OperationType.ScrubDecompress;

                RegisterOperation(operation, true);

                EnsurePcOnlyThread();
            }

            UnlockOperationList();
        }

        //----------------- Renomeia um jogo
        public static void Rename(String file, string newname, string newcode)
        {
            bool failed = false;

            IDrive drive = DeviceManager.GetDrive(Path.GetPathRoot(file));
            if ((drive != null) && drive.Ready)
            {
                if (drive.Is(DriveFlags.WBFS))
                {
                    WBFSDevice device = ((WBFSDrive)drive)._Device;
                    IDisc disc = device.GetDiscByCode(Path.GetFileName(file));
                    if (disc == null)
                        failed = true;
                    else
                    {
                        if (device.ChangeDiscHeader(disc, newname, newcode) != 0) failed = true;
                    }
                }
                else
                {
                    String path = Path.GetDirectoryName(file);
                    if (!path.EndsWith("\\")) path += '\\';
                    path += newcode + " - " + FixName(newname);
                    
                    if (ScrubDisc.IsScrubDisc(file) == 0)
                    {
                        path += ScrubDisc.ext_wiiscrub;
                        file = IOManager.Move2(file, path);
                        if (file.Length > 0)
                        {
                            if (ScrubDisc.SetScrubInfo(file, newcode, newname) != 0)
                                failed = true;
                        }
                        else
                        {
                            Log.LogLine(Language.log_file_not_found, LogMessageType.Warning, file);
                            failed = true;
                        }
                    }
                    else
                    {
                        path += WiiDisc.ext_wiiiso;
                        file = IOManager.Move2(file, path);
                        if (file.Length > 0)
                        {
                            if (WiiDisc.SetIsoInformation(file, newcode, newname) != 0)
                                failed = true;
                        }
                        else
                        {
                            Log.LogLine(Language.log_file_not_found, LogMessageType.Warning, file);
                            failed = true;
                        }
                    }
                }
            }
            else
            {
                MessageBox.Show(Language.message_drive_not_ready, Language.AppName, MessageBoxButtons.OK);
            }

            if (failed)
            {
                MessageBox.Show(Language.message_failed_rename_files, Language.AppName, MessageBoxButtons.OK);
            }
        }

        //----------------- Apaga um arquivo
        public static void Delete(String[] files)
        {
            if (files.Length == 0) return;

            if (MessageBox.Show(Language.message_delete_files, Language.AppName, MessageBoxButtons.YesNo,
                MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.No)
            {
                return;
            }

            bool failed = false;
            bool drivenotready = false;

            foreach (string file in files)
            {
                IDrive drive = DeviceManager.GetDrive(Path.GetPathRoot(file));
                if ((drive != null) && drive.Ready)
                {
                    if (drive.Is(DriveFlags.WBFS))
                    {
                        WBFSDevice device = ((WBFSDrive)drive)._Device;
                        if (device.DeleteDisc(Path.GetFileName(file)) != 0) failed = true;
                        drive.Refresh();
                    }
                    else
                    {
                        if (IOManager.Delete(file) != 0)
                            failed = true;
                    }
                }
                else
                {
                    drivenotready = true;
                }
            }

            if (failed)
            {
                MessageBox.Show(Language.message_failed_delete_files, Language.AppName, MessageBoxButtons.OK);
            }

            if (drivenotready)
            {
                MessageBox.Show(Language.message_drive_not_ready, Language.AppName, MessageBoxButtons.OK);
            }
        }

        //----------------- Ripa um DVD, é apenas uma adaptação do MoveFiles(...)
        public static void RipDvd(String drive, String destination)
        {
            //Caminho vazio não...
            if (destination.Length == 0) return;

            //Valida o caminho de destino
            if ((destination = FixPath(destination)) == String.Empty) return;

            LockOperationList();

            CDDrive cddrive = (CDDrive)DeviceManager.GetDrive(drive);

            Operation operation = new Operation(GenerateUid(true));
            operation.Type = OperationType.Copy;

            operation.SourcePath = drive + cddrive._Code + " - " + cddrive._Name;
            operation.SourceDrive = cddrive;

            operation.DestinationPath = destination;
            operation.DestinationDrive = DeviceManager.GetDrive(Path.GetPathRoot(destination));

            //
            RegisterOperation(operation, true);

            if (operation.DestinationDrive.Is(DriveFlags.WBFS))
            {
                EnsureWBFSThread(operation.DestinationDrive.Name);
            }
            else
            {
                EnsurePcOnlyThread();
            }

            UnlockOperationList();
        }

        //----------------- Gerenciadores principais de operação

        //----------------- Rotina assíncrona de gerenciamento de operações não WBFS,
        //Deve existir apenas uma instância
        static void PcOnlyWorker()
        {
            //Você não é tão importante por enquanto...
            Thread.CurrentThread.Priority = ThreadPriority.Lowest;
            int i = 0;
            while (!ShutDown)
            {
                Thread.Sleep(10);

                //Retorna
                if (PcOnlyOperations == 0)
                {
                    PcOnlyThread.Working = false;
                    return;
                }

                //Mantém a lista 'rodando'
                Operation operation = GetPcOnlyOperation(i++);
                if (operation == null)
                {
                    i = 0;
                    continue;
                }

                //Verifica se o arquivo de origem existe
                if (!FileExists(operation.SourcePath))
                {
                    //Erro crítico, termina a operação, loga erro
                    FinishOperation(operation);
                }

                //Verifica se os drives de origem e destino estão prontos
                if ((operation.SourceDrive != null) && !operation.SourceDrive.Ready)
                {
                    //Também é um erro crítico, termina a operação, loga erro
                    Log.LogLine(Language.log_drive_not_ready, LogMessageType.Error, operation.SourceDrive.Name);
                    FinishOperation(operation);
                }

                if ((operation.DestinationDrive != null) && !operation.DestinationDrive.Ready)
                {
                    //Também é um erro crítico, termina a operação, loga erro
                    Log.LogLine(Language.log_drive_not_ready, LogMessageType.Error, operation.DestinationDrive.Name);
                    FinishOperation(operation);
                }

                //Se não puder executar a operação volta
                //para o inicio do loop para testar outras threads
                if (!CanDoOperation(operation))
                {
                    //Libera o InUse da operação
                    FreeOperation(operation);
                    continue;
                }

                //Agora sim...
                Thread.CurrentThread.Priority = ThreadPriority.Normal;

                // 2 tipos de operações PC->PC suportadas: Ripar DVD e Copiar isos, em ambas SourcePath, SourceDrive,
                // DestinationPath e DestinationDrive são não-nulos

                //Verifica se a origem é um Drive de DVD com um disco de Wii Dentro
                CDDrive cddrive = operation.SourceDrive as CDDrive;
                if ((cddrive != null) && cddrive._WiiDiscInserted)
                {
                    //Ripa o DVD
                    if (ExtractDiscScrubbed)
                    {
                        ThreadManager.LaunchThread("Operation.Rp2Scrb", new ParameterizedThreadStart(RipDvdToScrubRoutine), 
                            operation);
                    }
                    else
                    {
                        ThreadManager.LaunchThread("Operation.Rp2Pc", new ParameterizedThreadStart(RipDvdToPcRoutine), 
                            operation);
                    }
                }
                else
                {
                    switch (operation.Type)
                    {
                        case OperationType.ScrubCompress:
                            ThreadManager.LaunchThread("Operation.Pc2Scrb", new ParameterizedThreadStart(PcToScrubRoutine), 
                                operation);
                            break;

                        case OperationType.ScrubDecompress:
                            ThreadManager.LaunchThread("Operation.Scrb2Pc", new ParameterizedThreadStart(ScrubToPcRoutine), 
                                operation);
                            break;

                        default:
                            ThreadManager.LaunchThread("Operation.Pc2Pc", new ParameterizedThreadStart(PcToPcRoutine), 
                                operation);
                            break;

                    }
                }

                //Volta a ter pouca prioridade...
                Thread.CurrentThread.Priority = ThreadPriority.Lowest;
            }
        }

        //----------------- Rotina assíncrona de gerenciamento de operações WBFS,
        //cada drive wbfs deve possuir uma instância
        static void WbfsWorker(Object o)
        {
            String me = (string)o;
            int myindex = 0;
            for (int k = 0; k < WbfsThreads.Length; k++)
            {
                if (String.Compare(WbfsThreads[k].Name, me, true) == 0)
                {
                    myindex = k;
                    break;
                }
            }

            //Você não é tão importante por enquanto...
            Thread.CurrentThread.Priority = ThreadPriority.Lowest;
            int i = 0;
            while (!ShutDown)
            {
                Thread.Sleep(10);

                //Retorna
                if (CountWbfsOperations(me) == 0)
                {
                    break;
                }

                //Mantém a lista 'rodando'
                Operation operation = GetWbfsOperation(i++, me);
                if (operation == null)
                {
                    i = 0;
                    continue;
                }

                //Verifica se o arquivo de origem existe
                if (!FileExists(operation.SourcePath))
                {
                    //Erro crítico, termina a operação, loga erro
                    FinishOperation(operation);
                }

                //Verifica se os drives de origem e destino estão prontos
                if ((operation.SourceDrive != null) && !operation.SourceDrive.Ready)
                {
                    //Também é um erro crítico, termina a operação, loga erro
                    Log.LogLine(Language.log_drive_not_ready, LogMessageType.Error, operation.SourceDrive.Name);
                    FinishOperation(operation);
                }

                if ((operation.DestinationDrive != null) && !operation.DestinationDrive.Ready)
                {
                    //Também é um erro crítico, termina a operação, loga erro
                    Log.LogLine(Language.log_drive_not_ready, LogMessageType.Error, operation.DestinationDrive.Name);
                    FinishOperation(operation);
                }

                //Se não puder executar a operação volta
                //para o inicio do loop para testar outras threads
                if (!CanDoOperation(operation))
                {
                    //Libera o InUse da operação
                    FreeOperation(operation);
                    continue;
                }

                //Agora sim...
                Thread.CurrentThread.Priority = ThreadPriority.Normal;

                // 2 tipos de operações PC->PC suportadas: Ripar DVD e Copiar isos, em ambas SourcePath, SourceDrive,
                // DestinationPath e DestinationDrive são não-nulos

                if (operation.IsSourceWBFS)
                {
                    //Drive atual é o de origem
                    if (String.Compare(me, operation.SourceDrive.Name, true) == 0)
                    {
                        //Extrai o disco para o PC como Scrub
                        if ((ExtractDiscScrubbed) || operation.Type == OperationType.ScrubCompress)
                        {
                            WbfsToScrubRoutine(operation);
                        }
                        else //Extrai o disco para o PC como ISO
                        {
                            WbfsToPcRoutine(operation);
                        }
                    }
                    else
                    {
                        //Adiciona disco para o drive atual
                        WbfsToWbfsRoutine(operation);
                    }

                }
                else
                {
                    CDDrive cddrive = operation.SourceDrive as CDDrive;
                    if ((cddrive != null) && cddrive._WiiDiscInserted)
                    {
                        //Ripa o DVD para o drive wbfs
                        RipDvdToWbfsRoutine(operation);
                    }
                    else if (ScrubDisc.IsScrubDisc(operation.SourcePath) == 0)
                    {
                        //Envia o disco scrub para o drive wbfs
                        ScrubToWbfsRoutine(operation);
                    }
                    else
                    {
                        //Tenta enviar o arquivo como iso para o drive wbfs
                        PcToWbfsRoutine(operation);
                    }
                }

                //Volta a ter pouca prioridade...
                Thread.CurrentThread.Priority = ThreadPriority.Lowest;
            }

            WbfsThreads[myindex].Working = false;
        }

        //----------------- Rotinas de operação

        //----------------- Callback de funções nativas de cópia de arquivos
        static CopyProgressResult CopyProgressHandler(long total, long transferred, long streamSize, long StreamByteTrans,
            uint dwStreamNumber, CopyProgressCallbackReason reason, int hSourceFile, int hDestinationFile, Operation lpData)
        {
            Operation op = lpData;

            //Termina a operação caso ela seja cancelada
            if (op.Progress.Cancel)
            {
                return CopyProgressResult.PROGRESS_CANCEL;
            }

            //Atualiza o contador de progresso
            op.Progress.Total = total;
            op.Progress.Progress = transferred;

            return CopyProgressResult.PROGRESS_CONTINUE;
        }

        //----------------- Rotina de sincronização de capas
        static void ExportCoversRoutine(Object obj)
        {
            Operation operation = (Operation)obj;

            //------ Não ativa a operação usando o ActivateOperation
            operation.Waiting = false;
            activeOperations++;

            LoaderHomebrew loader = (LoaderHomebrew)operation.ExtendedInfo;
            String[] covers = Covers.AvaliableCovers.ToArray(); //Impede que apagar uma capa com essa rotina ativa
            //cause um indice fora de intervalo: 
            //i >= AvaliableCovers.Count

            int exportWidth, exportHeight;
            if (loader == null)
            {
                exportWidth = Covers.DefaultWidth;
                exportHeight = Covers.DefaultHeight;
            }
            else
            {
                exportWidth = loader.CoverWidth;
                exportHeight = loader.CoverHeight;
            }

            operation.Progress.Reset(covers.Length, WBFSDevice.wiiSectorSize);

            String dest = operation.DestinationPath;
            if (!Directory.Exists(dest)) Directory.CreateDirectory(dest);

            int i = 0;
            while ((i < covers.Length) && (!operation.Progress.Cancel))
            {
                operation.Progress.Progress = i;
                operation.SourcePath = Covers.CoversDir + Path.GetFileName(covers[i]);
                operation.DestinationPath = dest + Path.GetFileName(covers[i]);

                try
                {
                    if (File.Exists(operation.SourcePath))
                    {
                        ResizeImage.ResizeFile(operation.SourcePath, operation.DestinationPath, exportWidth, exportHeight);
                    }
                    else
                    {
                        Log.LogLine(Language.log_file_not_found, LogMessageType.Warning, Path.GetFileName(covers[i]));
                    }
                }
                catch (Exception e)
                {
                    Log.LogException(e);
                }

                i++;
            }

            FinishOperation(operation);
        }

        //----------------- Rotina de download de capas
        static void CoverDownloadRoutine(Object obj)
        {
            Operation operation = (Operation)obj;

            //------ Não ativa a operação usando o ActivateOperation
            operation.Waiting = false;
            activeOperations++;

            //-------------------------------------

            Covers.CoverDownloadActive = true;

            int i = 0;
            while ((i < Covers.CoversToDownload.Count) && (!operation.Progress.Cancel))
            {
                operation.Progress.Total = Covers.CoversToDownload.Count;
                operation.Progress.Progress = i;
                operation.DestinationPath = Covers.CoversDir + Covers.CoversToDownload[i] + ".png";

                Covers.DownloadCover(Covers.CoversToDownload[i++], operation.Progress);
                Thread.Sleep(1);
            }

            //-------------------------------------

            Covers.CoversToDownload.Clear();

            FinishOperation(operation);

            Covers.CoverDownloadActive = false;
            Covers.OnCoverListChanged();
        }

        //----------------- Rotina de verificação e atualização
        static void ProgramUpdateRoutine(Object obj)
        {
            Operation operation = (Operation)obj;

            //------ Não ativa a operação usando o ActivateOperation
            operation.Waiting = false;
            activeOperations++;

            //-------------------------------------

            ProgramUpdate.UpdateActive = true;

            int version = ProgramUpdate.GetSubversionList();
            if (version == -1) goto END; //Erro

            if (ProgramUpdate.CurrentVersion >= version)
            {
                Log.LogLine(Language.log_update_latest_version, LogMessageType.Message);
                goto END;
            }

            if (MessageBox.Show(String.Format(Language.message_update_new_version, ProgramUpdate.SVersion), Language.AppName,
                    MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
            {
                goto END;
            }

            if (ProgramUpdate.NotifyOnly)
            {
                Process.Start(@"http://wbfssync.codeplex.com");
                goto END;
            }

            //Baixa os arquivos do servidor SVN do CodePlex

            long totalsize = 0;
            
            foreach (COREFILE file in ProgramUpdate.CoreFiles)
            {
                totalsize += file.Size;
            }

            //Baixa todos os idiomas
            foreach (LANGUAGEFILE file in ProgramUpdate.LanguageFiles)
            {
                totalsize += file.Size;
            }

            operation.Progress.Reset(totalsize, 1);
            string outdir = ProgramUpdate.UpdateFolder + ProgramUpdate.SVersion + '\\';

            if (!Directory.Exists(outdir))
                Directory.CreateDirectory(outdir);

            //Changelog e License

            string serverfile = ProgramUpdate.UpdateHost + "changelog.txt";
            string outfile = outdir + "changelog.txt";
            operation.DestinationPath = "changelog.txt";

            if (ProgramUpdate.DownloadFile(serverfile, outfile, operation.Progress) != 0)
            {
                //Erro ao baixar o arquivo
                Log.LogLine(Language.log_update_cant_download, LogMessageType.Error, "changelog.txt");
                goto ERROR;
            }

            serverfile = ProgramUpdate.UpdateHost + "license.txt";
            outfile = outdir + "license.txt";
            operation.DestinationPath = "license.txt";

            if (ProgramUpdate.DownloadFile(serverfile, outfile, operation.Progress) != 0)
            {
                //Erro ao baixar o arquivo
                Log.LogLine(Language.log_update_cant_download, LogMessageType.Error, "license.txt");
                goto ERROR;
            }

            //-----------------------------

            foreach (COREFILE file in ProgramUpdate.CoreFiles)
            {
                if (operation.Progress.Cancel)
                {
                    Log.LogLine(Language.log_update_canceled, LogMessageType.Message);
                    goto ERROR;
                }

                //Formata o caminho no servidor e destino
                serverfile = ProgramUpdate.UpdateHost +
                    file.Path.Replace('\\', '/').Substring(file.Path[0] == '/' ? 1 : 0);
                outfile = outdir + Path.GetFileName(file.Path);

                operation.DestinationPath = Path.GetFileName(file.Path);

                if (ProgramUpdate.DownloadFile(serverfile, outfile, operation.Progress) != 0)
                {
                    //Erro ao baixar o arquivo
                    Log.LogLine(Language.log_update_cant_download, LogMessageType.Error, Path.GetFileName(file.Path));
                    goto ERROR;
                }

                MD5 hash = MD5.Create();
                Stream sfile = File.OpenRead(outfile);
                Byte[] bhash = hash.ComputeHash(sfile);
                sfile.Close();

                string shash = "";
                for (int i = 0; i < bhash.Length; i++) shash += bhash[i].ToString("X");
                bhash = null;

                if (String.Compare(file.MD5, shash, true) != 0)
                {
                    //Erro de verificação de Hashing
                    Log.LogLine(Language.log_update_cant_download, LogMessageType.Error, Path.GetFileName(file.Path));
                    goto ERROR;
                }
            }

            outdir = Language.LanguageFolder;

            if (!Directory.Exists(outdir))
                Directory.CreateDirectory(outdir);

            //Baixa os idiomas, como eles são usados apenas quando o programa inicia e seu
            //conteúdo é cumulativo então substitui diretamente o arquivo
            foreach (LANGUAGEFILE file in ProgramUpdate.LanguageFiles)
            {
                if (operation.Progress.Cancel)
                {
                    Log.LogLine(Language.log_update_canceled, LogMessageType.Message);
                    goto ERROR;
                }

                //Formata o caminho no servidor e destino
                serverfile = ProgramUpdate.UpdateHost +
                    file.Path.Replace('\\', '/').Substring(file.Path[0] == '/' ? 1 : 0);
                outfile = outdir + Path.GetFileName(file.Path);

                operation.DestinationPath = Path.GetFileName(file.Path);

                if (ProgramUpdate.DownloadFile(serverfile, outfile, operation.Progress) != 0)
                {
                    //Erro ao baixar o arquivo
                    Log.LogLine(Language.log_update_cant_download, LogMessageType.Error, Path.GetFileName(file.Path));
                    goto ERROR;
                }
                else if (operation.Progress.Cancel)
                {
                    Log.LogLine(Language.log_update_canceled, LogMessageType.Message);
                    goto ERROR;
                }
            }

            //Copia o Updater.exe para o diretório do programa

            outdir = ProgramUpdate.UpdateFolder + ProgramUpdate.SVersion + '\\';
            if (File.Exists(outdir + "updater.exe"))
            {
                File.Delete("updater.exe");
                File.Move(outdir + "updater.exe", "updater.exe");
            }

            //

            MessageBox.Show(Language.message_update_completed, Language.AppName, MessageBoxButtons.OK, MessageBoxIcon.Information);
            Log.LogLine(Language.log_update_success, LogMessageType.Message);
            goto END;

            //-------------------------------------

            
            ERROR:

            Directory.Delete(outdir, true);

            END:

            FinishOperation(operation);
            ProgramUpdate.UpdateActive = false;
            ProgramUpdate.LastUpdate = DateTime.Now;
        }

        //----------------- Copia um arquivo de uma partição não wbfs para outra não wbfs
        static void PcToPcRoutine(Object o)
        {
            Operation operation = (Operation)o;
            ActivateOperation(operation);

            //-------------------

            operation.DestinationPath += Path.GetFileName(operation.SourcePath);

            //Calcula o tamanho do arquivo

            FileInfo fi = new FileInfo(operation.SourcePath);
            long size = fi.Length;
            fi = null;

            size = EnsureFreeSpace(operation.DestinationPath, size);

            //Inicia cópia

            bool result = false;

            switch (operation.Type)
            {
                case OperationType.Copy:

                    if (size < 0)
                    {
                        Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                        goto ENDOPERATION;
                    }

                    result = CopyFileEx(operation.SourcePath, operation.DestinationPath, CopyProgressHandler, 
                        operation, ref operation.Progress.cancel, CopyFileFlags.None);

                    break;

                case OperationType.Move:

                    if((operation.SourceDrive != operation.DestinationDrive) && (size < 0))
                    {
                        Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                        goto ENDOPERATION;
                    }

                    result = MoveFileWithProgress(operation.SourcePath, operation.DestinationPath, CopyProgressHandler,
                        operation, MoveFileFlags.MOVE_FILE_COPY_ALLOWED | MoveFileFlags.MOVE_FILE_REPLACE_EXISTSING);

                    break;
            }

            ENDOPERATION:

            FinishOperation(operation);
        }

        //----------------- Copia um arquivo de uma partição não wbfs para uma wbfs
        static void PcToWbfsRoutine(Object o)
        {
            Operation operation = (Operation)o;
            ActivateOperation(operation);

            //-------------------

            WBFSDevice destination = ((WBFSDrive)operation.DestinationDrive)._Device;

            //-------------------

            //Calcula o tamanho do arquivo, a operação falha se o arquivo não puder ser acessado
            long size = destination.CalculateDiscSize(operation.SourcePath);

            if ((size < 0) || (size > destination.Free))
            {
                Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                goto ENDOPERATION;
            }

            //Inicia a operação

            int r = destination.AddDisc(operation.SourcePath, operation.Progress, false);

            //A operação foi bem sucedida e a operação era de recorte, apaga o arquivo de origem
            if ((r == 0) && operation.Type == OperationType.Move)
            {
                IOManager.Delete(operation.SourcePath);
            }

        ENDOPERATION:

            FinishOperation(operation);
        }

        //----------------- Compacta um arquivo de uma partição não wbfs para outra não wbfs
        static void PcToScrubRoutine(Object o)
        {
            Operation operation = (Operation)o;
            ActivateOperation(operation);

            //-------------------

            operation.DestinationPath += Path.GetFileNameWithoutExtension(operation.SourcePath) + ScrubDisc.ext_wiiscrub;

            //Calcula o tamanho do arquivo, a operação falha se o arquivo não puder ser acessado
            long size = ScrubDisc.CalculateDiscSize(operation.SourcePath);

            if ((size < 0) || (size > operation.DestinationDrive.FreeSpace))
            {
                Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                goto ENDOPERATION;
            }

            //Inicia a operação
            ScrubDisc.CreateScrub(operation.SourcePath, operation.DestinationPath, operation.Progress, ScrubPartition);

        ENDOPERATION:

            //A operação é síncrona, finaliza a operação.

            FinishOperation(operation);

        }

        //----------------- Ripa um jogo do DVD para uma partição não wbfs
        static void RipDvdToPcRoutine(Object o)
        {
            Operation operation = (Operation)o;
            ActivateOperation(operation);

            //-------------------

            CDDrive drive = (CDDrive)operation.SourceDrive;
            operation.DestinationPath += drive._Code + " - " + FixName(drive._Name) + WiiDisc.ext_wiiiso;

            //Calcula o tamanho do arquivo, a operação falha se o arquivo não puder ser acessado
            long size = EnsureFreeSpace(operation.DestinationPath, drive._Size);
            if (size < 0)
            {
                Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                goto ENDOPERATION;
            }

            //Inicia a cópia

            DVDRomReader reader = new DVDRomReader(drive.Name[0]);
            IIOContext writer = null;

            if (reader.Result != 0) goto FREE;
            if (reader.Lock() != IORet.RET_IO_OK) goto FREE;
            if (reader.PrepareForAsync() != IORet.RET_IO_OK) goto FREE;

            writer = IOManager.CreateIOContext("DVDRIP", operation.DestinationPath, FileAccess.Write,
                FileShare.Read, 0, FileMode.OpenOrCreate, EFileAttributes.None);

            if (writer.Result != 0) goto FREE;
            if (writer.Lock() != IORet.RET_IO_OK) goto FREE;
            if (writer.PrepareForAsync() != IORet.RET_IO_OK) goto FREE;

            //
            //Cria os buffers para leitura/escrita assíncrona
            Byte[] buffer1 = new Byte[WBFSDevice.wiiSectorSize];
            Byte[] buffer2 = new Byte[WBFSDevice.wiiSectorSize];
            Byte[] zeroBuffer = new Byte[WBFSDevice.wiiSectorSize];

            Byte[] readBuffer = buffer1;
            Byte[] writeBuffer = buffer2;
            Array.Clear(zeroBuffer, 0, (int)WBFSDevice.wiiSectorSize);

            byte readBufferState = 0; //0 - vazio, 1 - lendo, 2 - cheio
            byte writeBufferState = 0; //0 - vazio, 1 - escrevendo, 2 - cheio

            int runto = (int)(reader.Length / WBFSDevice.wiiSectorSize);

            writer.Seek(reader.Length);
            writer.SetEOF();
            writer.Seek(0);

            reader.Seek(0);
            operation.Progress.Reset(runto, WBFSDevice.wiiSectorSize);

            int i = 0;
            while (!operation.Progress.Cancel)
            {
                if (!reader.CanWork) { operation.Progress.Cancel = true; break; }

                if (readBufferState == 0)
                {
                    reader.ReadAsync(readBuffer, (long)WBFSDevice.wiiSectorSize * i, (int)WBFSDevice.wiiSectorSize);
                    readBufferState = 1; //Lendo...
                }

                if ((readBufferState == 1) && (!reader.Working)) //Fim da leitura
                {
                    //Conserta a tabela de partição se necessário
                    if ((long)WBFSDevice.wiiSectorSize * i == 0x10000)
                    {
                        WiiDisc.FixPartitionTable(PartitionSelection.OnlyGamePartition, readBuffer);
                    }

                    readBufferState = 2; //Pronto
                }

                if ((writeBufferState == 1) && (!writer.Working)) //Fim da escrita
                {
                    writeBufferState = 0; //Livre
                    operation.Progress.Progress++;
                }

                if ((readBufferState == 2) && (writeBufferState == 0))
                {
                    //Troca os buffers
                    byte[] t = readBuffer;
                    readBuffer = writeBuffer;
                    writeBuffer = t;

                    writer.WriteAsync(writeBuffer, (int)WBFSDevice.wiiSectorSize);

                    writeBufferState = 1; //Escrevendo...
                    readBufferState = 0; //Permite o buffering de mais um setor

                    if (++i == runto) break; //Fim do disco
                }
            }

            //Garante que todas as operações assíncronas terminem
            while (writer.Working) System.Threading.Thread.Sleep(1);
            while (reader.Working) System.Threading.Thread.Sleep(1); //Só ocorrerá se a operação for cancelada

            //------------------------------------

            FREE:

            if (writer != null)
            {
                writer.FreeAsync();
                writer.Close(); //Libera o contexto do dispositivo
                writer = null;
            }

            if (reader != null)
            {
                reader.FreeAsync();
                reader.Close(); //Fecha o disco
                reader = null;
            }

            if (operation.Progress.Cancel) //Apaga o arquivo de destino
            {
                IOManager.Delete(operation.DestinationPath);
            }

        ENDOPERATION:

            //Limpa as matrizes
            readBuffer = null;
            writeBuffer = null;
            buffer1 = null;
            buffer2 = null;

            FinishOperation(operation);
        }

        //----------------- Ripa um jogo do DVD para uma partição wbfs
        static void RipDvdToWbfsRoutine(Object o)
        {
            Operation operation = (Operation)o;
            ActivateOperation(operation);

            //-------------------

            WBFSDevice destination = ((WBFSDrive)operation.DestinationDrive)._Device;
            CDDrive drive = (CDDrive)operation.SourceDrive;
            operation.DestinationPath += drive._Code;

            //-------------------

            //Calcula o tamanho do arquivo, a operação falha se o arquivo não puder ser acessado
            long size = destination.CalculateDiscSize(drive._WiiSectorUsageTable);
            if ((size < 0) || (size > destination.Free))
            {
                Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                goto ENDOPERATION;
            }

            destination.AddDisc(drive.Name[0], operation.Progress, false);

        ENDOPERATION:

            FinishOperation(operation);
        }

        //----------------- Ripa um jogo do DVD para um arquivo compactado em uma partição não wbfs
        static void RipDvdToScrubRoutine(Object o)
        {
            Operation operation = (Operation)o;
            ActivateOperation(operation);

            //-------------------

            CDDrive drive = (CDDrive)operation.SourceDrive;
            operation.DestinationPath += drive._Code + " - " + FixName(drive._Name) + ScrubDisc.ext_wiiscrub;

            //-------------------

            //Calcula o tamanho do arquivo, a operação falha se o arquivo não puder ser acessado
            long size = ScrubDisc.CalculateDiscSize(drive._WiiSectorUsageTable);
            if ((size < 0) || (size > operation.DestinationDrive.FreeSpace))
            {
                Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                goto ENDOPERATION;
            }

            ScrubDisc.CreateScrub(drive.Name[0], operation.DestinationPath, operation.Progress, ScrubPartition);

        ENDOPERATION:

            FinishOperation(operation);
        }

        //----------------- Copia um arquivo de uma partição wbfs para uma não wbfs
        static void WbfsToPcRoutine(Object o)
        {
            Operation operation = (Operation)o;
            ActivateOperation(operation);

            //-------------------

            WBFSDevice source = ((WBFSDrive)operation.SourceDrive)._Device;
            IDisc disc = source.GetDiscByCode(Path.GetFileName(operation.SourcePath));
            operation.DestinationPath += disc.Code + " - " + FixName(disc.Name) + WiiDisc.ext_wiiiso;

            //-------------------

            //Calcula o tamanho do arquivo, a operação falha se o arquivo não puder ser acessado
            long size = disc.Size;

            if ((size < 0) || (size > operation.DestinationDrive.FreeSpace))
            {
                Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                goto ENDOPERATION;
            }

            //Inicia a operação

            int r = source.ExtractDisc(operation.DestinationPath, disc, operation.Progress);

            //A operação foi bem sucedida e a operação era de recorte, apaga o arquivo de origem
            if ((r == 0) && operation.Type == OperationType.Move)
            {
                disc.Delete();
            }

        ENDOPERATION:

            FinishOperation(operation);
        }

        //----------------- Copia um arquivo de uma partição wbfs para outra wbfs
        static void WbfsToWbfsRoutine(Object o)
        {
            Operation operation = (Operation)o;
            ActivateOperation(operation);

            //-------------------

            WBFSDevice source = ((WBFSDrive)operation.SourceDrive)._Device;
            WBFSDevice destination = ((WBFSDrive)operation.DestinationDrive)._Device;
            IDisc disc = source.GetDiscByCode(Path.GetFileName(operation.SourcePath));
            operation.DestinationPath += disc.Code;

            //-------------------

            //Calcula o tamanho do arquivo, a operação falha se o arquivo não puder ser acessado
            long size = disc.WBFSSize;

            if ((size < 0) || (size > operation.DestinationDrive.FreeSpace))
            {
                Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                goto ENDOPERATION;
            }

            //Inicia a operação

            int r = destination.AddDisc(disc, operation.Progress, false);

            //A operação foi bem sucedida e a operação era de recorte, apaga o arquivo de origem
            if ((r == 0) && operation.Type == OperationType.Move)
            {
                disc.Delete();
            }

        ENDOPERATION:

            FinishOperation(operation);
        }

        //----------------- Copia um arquivo de uma partição wbfs para um arquivo comprimido em uma partição não wbfs
        static void WbfsToScrubRoutine(Object o)
        {
            Operation operation = (Operation)o;
            ActivateOperation(operation);

            //-------------------

            WBFSDevice source = ((WBFSDrive)operation.SourceDrive)._Device;
            IDisc disc = source.GetDiscByCode(Path.GetFileName(operation.SourcePath));
            operation.DestinationPath += disc.Code + " - " + FixName(disc.Name) + ScrubDisc.ext_wiiscrub;

            //-------------------

            //Calcula o tamanho do arquivo, a operação falha se o arquivo não puder ser acessado
            long size = disc.WBFSSize;

            if ((size < 0) || (size > operation.DestinationDrive.FreeSpace))
            {
                Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                goto ENDOPERATION;
            }

            //Inicia a operação

            int r = ScrubDisc.CreateScrub(disc, operation.DestinationPath, operation.Progress);

            //A operação foi bem sucedida e a operação era de recorte, apaga o arquivo de origem
            if ((r == 0) && operation.Type == OperationType.Move)
            {
                disc.Delete();
            }

        ENDOPERATION:

            FinishOperation(operation);
        }

        //----------------- Copia um arquivo comprimido de uma partição não wbfs para outra não wbfs
        static void ScrubToPcRoutine(Object o)
        {
            Operation operation = (Operation)o;
            ActivateOperation(operation);

            //-------------------

            ScrubDisc disc = new ScrubDisc();
            if (disc.Open(operation.SourcePath, false) != 0)
            {
                //Ainda não tem mensagem de erro para isso, o ScrubDisc e o IIOContext irão
                //logar os erros mais precisamente
                goto ENDOPERATION;
            }

            operation.DestinationPath += disc.Code + " - " + FixName(disc.Name) + WiiDisc.ext_wiiiso;

            //-------------------

            //Calcula o tamanho do arquivo, a operação falha se o arquivo não puder ser acessado
            long size = EnsureFreeSpace(operation.DestinationPath, disc.Length);

            if (size < 0)
            {
                Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                goto ENDOPERATION;
            }

            //Inicia a operação

            disc.Extract(operation.DestinationPath, operation.Progress);

        ENDOPERATION:

            if (disc != null) disc.Close();
            FinishOperation(operation);
        }

        //----------------- Copia um arquivo comprimido de uma partição não wbfs para uma wbfs
        static void ScrubToWbfsRoutine(Object o)
        {
            Operation operation = (Operation)o;
            ActivateOperation(operation);

            //-------------------

            WBFSDevice destination = ((WBFSDrive)operation.DestinationDrive)._Device;
            ScrubDisc disc = new ScrubDisc();

            if (disc.Open(operation.SourcePath, false) != 0)
            {
                //Ainda não tem mensagem de erro para isso, o ScrubDisc e o IIOContext irão
                //logar os erros mais precisamente
                goto ENDOPERATION;
            }

            operation.DestinationPath += disc.Code;

            //-------------------

            //Calcula o tamanho do arquivo, a operação falha se o arquivo não puder ser acessado
            long size = disc.UsedWbfsSectors * disc.WbfsSectorSize;

            if ((size < 0) || (size > operation.DestinationDrive.FreeSpace))
            {
                Log.LogLine(Language.log_not_enough_space, LogMessageType.Error, operation.SourcePath);
                goto ENDOPERATION;
            }

            //Inicia a operação

            destination.AddDisc(disc, operation.Progress, false);

        ENDOPERATION:

            if (disc != null) disc.Close();
            FinishOperation(operation);
        }

        //----------------- Clipboard do Windows

        //----------------- Adiciona Form na lista de handles que monitoram a clipboard
        public static void StartClipboardMonitoring(IntPtr handle)
        {
            thisMonitor = handle;
            nextMonitor = SetClipboardViewer(handle);
        }

        //----------------- Remove Form da lista de handles que monitoram a clipboard
        public static void StopClipboardMonitoring()
        {
            ChangeClipboardChain(thisMonitor, nextMonitor);
        }

        //----------------- Monitor da Clipboard do windows, pega as mensagens de cópia, recorte de arquivos
        public static void ProcessSystemMessage(int msg, IntPtr wparam, IntPtr lparam)
        {
            if (IgnoreWindowsClipboard) return;

            if (msg == WM_DRAWCLIPBOARD)
            {
                UpdateClipboard();
            }
            else if (msg == WM_CHANGECBCHAIN)
            {
                nextMonitor = lparam;
            }
        }

        //----------------- Atualiza o DiscClipboard
        public static void UpdateClipboard()
        {
            if (Clipboard.ContainsFileDropList())
            {
                try
                {
                    DiscClipboard = (String[])Clipboard.GetDataObject().GetData(DataFormats.FileDrop);
                }
                catch
                {
                    DiscClipboard = null;
                }

                if (DiscClipboard == null)
                {
                    DiscClipboard = new String[0];
                }
            }
            else
            {
                DiscClipboard = null;
                DiscClipboard = new String[0];
            }

            if (ClipboardChanged != null) ClipboardChanged(DiscClipboard);
        }

        //----------------- Rotinas nativas

        [DllImport("User32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr SetClipboardViewer(IntPtr hWndNewViewer);

        [DllImport("User32.dll", CharSet = CharSet.Auto)]
        private static extern int ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool CopyFileEx(string lpExistingFileName, string lpNewFileName,
            CopyProgressRoutine lpProgressRoutine, Operation lpData, ref Int32 pbCancel, CopyFileFlags dwCopyFlags);

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool MoveFileWithProgress(string lpExistingFileName, string lpNewFileName,
           CopyProgressRoutine lpProgressRoutine, Operation lpData, MoveFileFlags dwFlags);
    }
}
